home *** CD-ROM | disk | FTP | other *** search
/ SuperHack / SuperHack CD.bin / CODING / DELPHI / MIDICOM2.ZIP / MIDIOUT.PAS < prev    next >
Encoding:
Pascal/Delphi Source File  |  1996-05-01  |  19.0 KB  |  575 lines

  1. { $Header:   G:/delphi/midi/vcs/midiout.pas   1.9   30 Apr 1996 19:02:12   DAVEC  $ }
  2.  
  3. { Written by David Churcher <dchurcher@cix.compulink.co.uk>,
  4.   released to the public domain. }
  5.  
  6. { Thanks very much to Fred Kohler for the Technology code. }
  7.  
  8. unit MidiOut;
  9.  
  10. {
  11.   MIDI Output component.
  12.  
  13.   Properties:
  14.       DeviceID:     Windows numeric device ID for the MIDI output device.
  15.     Between 0 and (midioutGetNumDevs-1), or MIDI_MAPPER (-1).
  16.     Special value MIDI_MAPPER specifies output to the Windows MIDI mapper
  17.     Read-only while device is open, exception if changed while open
  18.  
  19.     MIDIHandle:    The output handle to the MIDI device.
  20.     0 when device is not open
  21.     Read-only, runtime-only
  22.  
  23.     ProductName: Name of the output device product that corresponds to the
  24.     DeviceID property (e.g. 'MPU 401 out').
  25.     You can write to this while the device is closed to select a particular
  26.     output device by name (the DeviceID property will change to match).
  27.     Exception if this property is changed while the device is open.
  28.  
  29.     Numdevs: Number of MIDI output devices installed on the system. This
  30.     is the value returned by midiOutGetNumDevs. It's included for
  31.     completeness.
  32.  
  33.     Technology: Type of technology used by the MIDI device. You can set this
  34.     property to one of the values listed for OutportTech (below) and the component
  35.     will find an appropriate MIDI device. For example:
  36.         MidiOutput.Technology := opt_FMSynth;
  37.     will set MidiInput.DeviceID to the MIDI device ID of the FM synth, if one
  38.     is installed. If no such device is available an exception is raised,
  39.     see MidiOutput.SetTechnology.
  40.  
  41.     See the MIDIOUTCAPS entry in MMSYSTEM.HLP for descriptions of the
  42.     following properties:
  43.         DriverVersion
  44.         Voices
  45.         Notes
  46.         ChannelMask
  47.         Support
  48.  
  49.     Error: The error code for the last MMSYSTEM error. See the MMSYSERR_
  50.     entries in MMSYSTEM.INT for possible values.
  51.  
  52.   Methods:
  53.     Open: Open MIDI device specified by DeviceID property for output
  54.  
  55.     Close: Close device
  56.  
  57.     PutMidiEvent(Event:TMyMidiEvent): Output a note or sysex message to the
  58.     device. This method takes a TMyMidiEvent object and transmits it.
  59.     Notes:
  60.       1. If the object contains a sysex event the OnMidiOutput event will
  61.           be triggered when the sysex transmission is complete.
  62.       2. You can queue up multiple blocks of system exclusive data for
  63.           transmission by chucking them at this method; they will be
  64.          transmitted as quickly as the device can manage.
  65.       3. This method will not free the TMyMidiEvent object, the caller
  66.           must do that. Any sysex data in the TMyMidiEvent is copied before
  67.          transmission so you can free the TMyMidiEvent immediately after
  68.          calling PutMidiEvent, even if output has not yet finished.
  69.  
  70.     PutShort(MidiMessage: Byte; Data1: Byte; Data2: Byte): Output a short
  71.     MIDI message. Handy when you can't be bothered to build a TMyMidiEvent.
  72.     If the message you're sending doesn't use Data1 or Data2, set them to 0.
  73.  
  74.     PutLong(SysexPointer: Pointer; msgLength: Word): Output sysex data.
  75.         SysexPointer: Pointer to sysex data to send
  76.         msgLength: Length of sysex data.
  77.     This is handy when you don't have a TMyMidiEvent.
  78.  
  79.     SetVolume(Left: Word, Right: Word): Set the volume of the
  80.     left and right channels on the output device (only on internal devices?).
  81.     0xFFFF is maximum volume. If the device doesn't support separate
  82.     left/right volume control, the value of the Left parameter will be used.
  83.     Check the Support property to see whether the device supports volume
  84.     control. See also other notes on volume control under midiOutSetVolume()
  85.     in MMSYSTEM.HLP.
  86.  
  87.   Events:
  88.     OnMidiOutput: Procedure called when output of a system exclusive block
  89.     is completed.
  90.  
  91.   Notes:
  92.    I haven't implemented any methods for midiOutCachePatches and
  93.   midiOutCacheDrumpatches, mainly 'cause I don't have any way of testing
  94.   them. Does anyone really use these?
  95. }
  96.  
  97. interface
  98.  
  99. uses
  100.   SysUtils, WinTypes, WinProcs, Messages, Classes, Controls, Forms,
  101.   MMSystem, Circbuf, MidiType, MidiDefs;
  102.  
  103. type
  104.     midioutputState = (mosOpen, mosClosed);
  105.     EmidioutputError = class(Exception);
  106.  
  107.     { These are the equivalent of constants prefixed with mod_
  108.       as defined in MMSystem. See SetTechnology }
  109.      OutPortTech = (
  110.       opt_None,              { none }
  111.       opt_MidiPort,        { output port }
  112.       opt_Synth,          { generic internal synth }
  113.       opt_SQSynth,        { square wave internal synth }
  114.       opt_FMSynth,        { FM internal synth }
  115.       opt_Mapper);        { MIDI mapper }
  116.     TechNameMap = array[OutPortTech] of string[18];
  117.  
  118.  
  119. const
  120.       TechName : TechNameMap = (
  121.         'None','MIDI Port','Generic Synth','Square Wave Synth',
  122.         'FM Synth','MIDI Mapper');
  123.  
  124. {-------------------------------------------------------------------}
  125. type
  126.   TMidiOutput = class(TComponent)
  127.   protected
  128.       Handle: THandle;                { Window handle used for callback notification }
  129.     FDeviceID: Integer;                { MIDI device ID }
  130.     FMIDIHandle: Hmidiout;        { Handle to output device }
  131.     FState: midioutputState;    { Current device state }
  132.     PCtlInfo: PMidiCtlInfo;    { Pointer to control info for DLL }
  133.  
  134.     PBuffer: PCircularBuffer;    { Output queue for PutTimedEvent, set by Open }
  135.  
  136.     FError: Word;    { Last MMSYSTEM error }
  137.  
  138.     { Stuff from midioutCAPS }
  139.     FDriverVersion: Version;    { Driver version from midioutGetDevCaps }
  140.     FProductName: string;         { product name }
  141.     FTechnology: OutPortTech;    { Type of MIDI output device }
  142.     FVoices: Word;                { Number of voices (internal synth) }
  143.     FNotes: Word;                { Number of notes (internal synth) }
  144.     FChannelMask: Word;            { Bit set for each MIDI channels that the
  145.                                   device responds to (internal synth) }
  146.     FSupport: DWORD;            { Technology supported (volume control,
  147.                                   patch caching etc. }
  148.     FNumdevs: Word;                { Number of MIDI output devices on system }
  149.  
  150.  
  151.     FOnMIDIOutput: TNotifyEvent;    { Sysex output finished }
  152.  
  153.     procedure MidiOutput(var Message: TMessage);
  154.     procedure SetDeviceID(DeviceID: Integer);
  155.     procedure SetProductName( NewProductName: String );
  156.     procedure SetTechnology( NewTechnology : OutPortTech );
  157.     function midioutErrorString( WError: Word ): String;
  158.  
  159.   public
  160.     { Properties }
  161.     property MIDIHandle: Hmidiout read FMIDIHandle;
  162.     property DriverVersion: Version     { Driver version from midioutGetDevCaps }
  163.                 read FDriverVersion;
  164.     property Technology: OutPortTech             { Type of MIDI output device }
  165.         read FTechnology
  166.                 write SetTechnology
  167.                 default opt_Synth;
  168.     property Voices: Word                { Number of voices (internal synth) }
  169.                 read FVoices;
  170.     property Notes: Word                { Number of notes (internal synth) }
  171.                 read FNotes;
  172.     property ChannelMask: Word            { Bit set for each MIDI channels that the }
  173.                 read FChannelMask;      { device responds to (internal synth) }
  174.     property Support: DWORD            { Technology supported (volume control, }
  175.                 read FSupport;          { patch caching etc. }
  176.     property Error: Word read FError;
  177.     property Numdevs: Word read FNumdevs;
  178.  
  179.     { Methods }
  180.     function Open: Boolean; virtual;
  181.     function Close: Boolean; virtual;
  182.     procedure PutMidiEvent( theEvent: TMyMidiEvent); virtual;
  183.     procedure PutShort(MidiMessage: Byte; Data1: Byte; Data2: Byte); virtual;
  184.     procedure PutLong(SysexPointer: Pointer; msgLength: Word); virtual;
  185.     procedure SetVolume( Left: Word; Right:Word );
  186.     constructor Create(AOwner:TComponent); override;
  187.     destructor Destroy; override;
  188.  
  189.    { Some functions to decode and classify incoming messages would be nice }
  190.  
  191.   published
  192.     { TODO: Property editor with dropdown list of product names }
  193.     property ProductName: String read FProductName write SetProductName;
  194.  
  195.     property DeviceID: Integer read FDeviceID write SetDeviceID default 0;
  196.     { TODO: midiOutGetVolume? Or two properties for Left and Right volume?
  197.       Is it worth it??
  198.         midiOutMessage?? Does anyone use this? }
  199.  
  200.     { Events }
  201.     property Onmidioutput: TNotifyEvent
  202.         read FOnmidioutput
  203.         write FOnmidioutput;
  204. end;
  205.  
  206. procedure Register;
  207.  
  208. {-------------------------------------------------------------------}
  209. implementation
  210.  
  211. { This is the callback procedure in the external DLL. 
  212.   It's used when midioutOpen is called by the Open method. 
  213.   There are special requirements and restrictions for this callback
  214.   procedure (see midioutOpen in MMSYSTEM.HLP) so it's impractical to 
  215.   make it an object method }
  216. {$IFDEF WIN32}
  217. function midiHandler(
  218.           hMidiIn: HMidiIn;
  219.           wMsg: UINT;
  220.           dwInstance: DWORD;
  221.           dwParam1: DWORD;
  222.           dwParam2: DWORD): Boolean; stdcall; external 'DELMID32.DLL';
  223. {$ELSE}
  224. function midiHandler(
  225.           hMidiIn: HMidiIn;
  226.           wMsg: Word;
  227.           dwInstance: DWORD;
  228.           dwParam1: DWORD;
  229.           dwParam2: DWORD): Boolean; far; external 'DELPHMID.DLL';
  230. {$ENDIF}
  231.  
  232.  
  233. {-------------------------------------------------------------------}
  234. constructor Tmidioutput.Create(AOwner:TComponent);
  235. begin
  236.     inherited Create(AOwner);
  237.     FState := mosClosed;
  238.     FNumdevs := midiOutGetNumDevs;
  239.  
  240.     { Create the window for callback notification }
  241.     if not (csDesigning in ComponentState) then
  242.         begin
  243.         Handle := AllocateHwnd(MidiOutput);
  244.         end;
  245.  
  246. end;
  247.  
  248. {-------------------------------------------------------------------}
  249. destructor Tmidioutput.Destroy;
  250. begin
  251.     if FState = mosOpen then
  252.         Close;
  253.     if (PCtlInfo <> Nil) then
  254.         GlobalSharedLockedFree( PCtlinfo^.hMem, PCtlInfo );
  255.     DeallocateHwnd(Handle);
  256.     inherited Destroy;
  257. end;
  258.  
  259. {-------------------------------------------------------------------}
  260. { Convert the numeric return code from an MMSYSTEM function to a string
  261.   using midioutGetErrorText. TODO: These errors aren't very helpful
  262.   (e.g. "an invalid parameter was passed to a system function") so
  263.   some proper error strings would be nice. }
  264. function Tmidioutput.midioutErrorString( WError: Word ): String;
  265. var
  266.     errorDesc: PChar;
  267. begin
  268.     try
  269.         errorDesc := StrAlloc(MAXERRORLENGTH);
  270.         if midioutGetErrorText(WError, errorDesc, MAXERRORLENGTH) = 0 then
  271.             result := StrPas(errorDesc)
  272.         else
  273.             result := 'Specified error number is out of range';
  274.     finally
  275.         StrDispose(errorDesc);
  276.     end;
  277. end;
  278.  
  279. {-------------------------------------------------------------------}
  280. { Set the output device ID and change the other properties to match }
  281. procedure Tmidioutput.SetDeviceID(DeviceID: Integer);
  282. var
  283.     midioutCaps: TmidioutCaps;
  284. begin
  285.     if FState = mosOpen then
  286.         raise EmidioutputError.Create('Change to DeviceID while device was open')
  287.     else
  288.         if (DeviceID >= midioutGetNumDevs) And (DeviceID <> MIDI_MAPPER) then
  289.             raise EmidioutputError.Create('Invalid device ID')
  290.         else
  291.             begin
  292.             FDeviceID := DeviceID;
  293.  
  294.             { Set the name and other midioutCAPS properties to match the ID }
  295.             FError :=
  296.                 midioutGetDevCaps(DeviceID, @midioutCaps, sizeof(TmidioutCaps));
  297.             if Ferror > 0 then
  298.                 raise EmidioutputError.Create(midioutErrorString(FError));
  299.  
  300.             with midiOutCaps do
  301.                 begin
  302.                 FProductName := StrPas(szPname);
  303.                 FDriverVersion := vDriverVersion;
  304.                 FTechnology := OutPortTech(wTechnology);
  305.                 FVoices := wVoices;
  306.                 FNotes := wNotes;
  307.                 FChannelMask := wChannelMask;
  308.                 FSupport := dwSupport;
  309.                 end;
  310.  
  311.             end;
  312. end;
  313.  
  314. {-------------------------------------------------------------------}
  315. { Set the product name property and put the matching output device number 
  316.   in FDeviceID.
  317.   This is handy if you want to save a configured output/output device
  318.   by device name instead of device number, because device numbers may
  319.   change if users install or remove MIDI devices.
  320.   Exception if output device with matching name not found,
  321.   or if output device is open }
  322. procedure Tmidioutput.SetProductName( NewProductName: String );
  323. var
  324.     midioutCaps: TmidioutCaps;
  325.     testDeviceID: Integer;
  326.     testProductName: String;
  327. begin
  328.     if FState = mosOpen then
  329.         raise EmidioutputError.Create('Change to ProductName while device was open')
  330.     else
  331.         { Don't set the name if the component is reading properties because
  332.         the saved Productname will be from the machine the application was compiled
  333.         on, which may not be the same for the corresponding DeviceID on the user's
  334.         machine. The FProductname property will still be set by SetDeviceID }
  335.         if not (csLoading in ComponentState) then
  336.              begin
  337.              { Loop uses -1 to test for MIDI_MAPPER as well }
  338.              for testDeviceID := -1 To (midioutGetNumDevs-1) do
  339.                  begin
  340.                  FError :=
  341.                      midioutGetDevCaps(testDeviceID, @midioutCaps, sizeof(TmidioutCaps));
  342.                  if Ferror > 0 then
  343.                      raise EmidioutputError.Create(midioutErrorString(FError));
  344.                  testProductName := StrPas(midioutCaps.szPname);
  345.                  if testProductName = NewProductName then
  346.                      begin
  347.                      FProductName := NewProductName;
  348.                      Break;
  349.                      end;
  350.                  end;
  351.              if FProductName <> NewProductName then
  352.                  raise EmidioutputError.Create('MIDI output Device ' +
  353.                      NewProductName + ' not installed')
  354.              else
  355.                  SetDeviceID(testDeviceID);
  356.          end;
  357. end;
  358.  
  359. {-------------------------------------------------------------------}
  360. { Set the output technology property and put the matching output device
  361.     number in FDeviceID.
  362.   This is handy, for example, if you want to be able to switch between a
  363.   sound card and a MIDI port }
  364. procedure TMidiOutput.SetTechnology( NewTechnology: OutPortTech );
  365. var
  366.     midiOutCaps: TMidiOutCaps;
  367.     testDeviceID: Integer;
  368.     testTechnology: OutPortTech;
  369. begin
  370.     if FState = mosOpen then
  371.         raise EMidiOutputError.Create(
  372.             'Change to Product Technology while device was open')
  373.     else
  374.         begin
  375.        { Loop uses -1 to test for MIDI_MAPPER as well }
  376.         for testDeviceID := -1 To (midiOutGetNumDevs-1) do
  377.             begin
  378.             FError :=
  379.                 midiOutGetDevCaps(testDeviceID,
  380.                     @midiOutCaps, sizeof(TMidiOutCaps));
  381.             if Ferror > 0 then
  382.                 raise EMidiOutputError.Create(MidiOutErrorString(FError));
  383.             testTechnology := OutPortTech(midiOutCaps.wTechnology);
  384.             if testTechnology = NewTechnology then
  385.                 begin
  386.                 FTechnology := NewTechnology;
  387.                 Break;
  388.                 end;
  389.             end;
  390.             if FTechnology <> NewTechnology then
  391.                 raise EMidiOutputError.Create('MIDI output technology ' +
  392.                     TechName[NewTechnology] + ' not installed')
  393.             else
  394.                 SetDeviceID(testDeviceID);
  395.         end;
  396. end;
  397.  
  398. {-------------------------------------------------------------------}
  399. function Tmidioutput.Open: Boolean;
  400. var
  401.     hMem: THandle;
  402. begin
  403.     try
  404.         { Create the control info for the DLL }
  405.         if (PCtlInfo = Nil) then
  406.             begin
  407.             PCtlInfo := GlobalSharedLockedAlloc( Sizeof(TMidiCtlInfo), hMem );
  408.             PctlInfo^.hMem := hMem;
  409.             end;
  410.  
  411.         Pctlinfo^.hWindow := Handle;    { Control's window handle }
  412.  
  413.         { FError := midioutOpen(@FMidiHandle, FDeviceId,
  414.                         DWORD(@midiHandler),
  415.                         DWORD(PCtlInfo),
  416.                         CALLBACK_FUNCTION); }
  417.                 FError := midioutOpen(@FMidiHandle, FDeviceId,
  418.                         Handle,
  419.                         DWORD(PCtlInfo),
  420.                         CALLBACK_WINDOW);
  421.         If (FError <> 0) then
  422.             { TODO: use CreateFmtHelp to add MIDI device name/ID to message }
  423.             raise EmidioutputError.Create(midioutErrorString(FError));
  424.  
  425.         FState := mosOpen;
  426.  
  427.     except
  428.         if PCtlInfo <> Nil then
  429.             begin
  430.             GlobalSharedLockedFree(PCtlInfo^.hMem, PCtlInfo);
  431.             PCtlInfo := Nil;
  432.             end;
  433.     end;
  434.  
  435. end;
  436.  
  437. {-------------------------------------------------------------------}
  438. procedure TMidiOutput.PutShort(MidiMessage: Byte; Data1: Byte; Data2: Byte);
  439. var
  440.     thisMsg: DWORD;
  441. begin
  442.     thisMsg := DWORD(MidiMessage) Or
  443.         (DWORD(Data1) shl 8) Or
  444.         (DWORD(Data2) shl 16);
  445.  
  446.     FError := midiOutShortMsg(FMidiHandle, thisMsg);
  447.     if Ferror > 0 then
  448.         raise EmidioutputError.Create(midioutErrorString(FError));
  449. end;
  450.  
  451. {-------------------------------------------------------------------}
  452. procedure TMidiOutput.PutLong(SysexPointer: Pointer; msgLength: Word);
  453. { Notes: This works asynchronously; you send your sysex output by
  454. calling this function, which returns immediately. When the MIDI device
  455. driver has finished sending the data the MidiOutPut function in this
  456. component is called, which will in turn call the OnMidiOutput method
  457. if the component user has defined one. }
  458. { TODO: Combine common functions with PutTimedLong into subroutine }
  459.  
  460. var
  461.     MyMidiHdr: TMyMidiHdr;
  462. begin
  463.     { Initialize the header and allocate buffer memory }
  464.     MyMidiHdr := TMyMidiHdr.Create(msgLength);
  465.  
  466.     { Copy the data over to the MidiHdr buffer
  467.       We can't just use the caller's PChar because the buffer memory
  468.       has to be global, shareable, and locked. }
  469.     StrMove(MyMidiHdr.SysexPointer, SysexPointer, msgLength);
  470.  
  471.     { Store the MyMidiHdr address in the header so we can find it again quickly
  472.       (see the MidiOutput proc) }
  473.     MyMidiHdr.hdrPointer^.dwUser := DWORD(MyMidiHdr);
  474.  
  475.     { Get MMSYSTEM's blessing for this header }
  476.     FError := midiOutPrepareHeader(FMidiHandle,MyMidiHdr.hdrPointer,
  477.         sizeof(TMIDIHDR));
  478.     if Ferror > 0 then
  479.         raise EMidiOutputError.Create(MidiOutErrorString(FError));
  480.  
  481.     { Send it }
  482.     FError := midiOutLongMsg(FMidiHandle, MyMidiHdr.hdrPointer,
  483.         sizeof(TMIDIHDR));
  484.     if Ferror > 0 then
  485.         raise EMidiOutputError.Create(MidiOutErrorString(FError));
  486.  
  487. end;
  488.  
  489. {-------------------------------------------------------------------}
  490. procedure Tmidioutput.PutMidiEvent(theEvent:TMyMidiEvent);
  491. var
  492.     thisMsg: DWORD;
  493. begin
  494.     if FState <> mosOpen then
  495.         raise EMidiOutputError.Create('MIDI Output device not open');
  496.  
  497.     with theEvent do
  498.         begin
  499.         if Sysex = Nil then
  500.             begin
  501.             PutShort(MidiMessage, Data1, Data2)
  502.             end
  503.         else
  504.             PutLong(Sysex, SysexLength);
  505.     end;
  506. end;
  507.  
  508. {-------------------------------------------------------------------}
  509. function Tmidioutput.Close: Boolean;
  510. begin
  511.     if FState = mosOpen then
  512.         begin
  513.  
  514.         FError := midioutReset(FMidiHandle);
  515.         if Ferror <> 0 then
  516.             raise EMidiOutputError.Create(MidiOutErrorString(FError));
  517.     
  518.         FError := midioutClose(FMidiHandle);
  519.         if Ferror <> 0 then
  520.             raise EMidiOutputError.Create(MidiOutErrorString(FError));
  521.         end;
  522.  
  523.     FMidiHandle := 0;
  524.     FState := mosClosed;
  525.  
  526. end;
  527.  
  528. {-------------------------------------------------------------------}
  529. procedure TMidiOutput.SetVolume( Left: Word; Right:Word );
  530. var
  531.     dwVolume: DWORD;
  532. begin
  533.     dwVolume := (DWORD(Left) shl 16) Or Right;
  534.     FError := midiOutSetVolume(DeviceID, dwVolume);
  535.     if Ferror <> 0 then
  536.         raise EMidiOutputError.Create(MidiOutErrorString(FError));
  537. end;
  538.  
  539. {-------------------------------------------------------------------}
  540. procedure Tmidioutput.midioutput( var Message: TMessage );
  541. { Triggered when sysex output from PutLong is complete }
  542. var
  543.     MyMidiHdr: TMyMidiHdr;
  544.     thisHdr: PMidiHdr;
  545. begin
  546.     if Message.Msg = Mom_Done then
  547.         begin
  548.         { Find the MIDIHDR we used for the output. Message.lParam is its address }
  549.         thisHdr := PMidiHdr(Message.lParam);
  550.  
  551.         { Remove it from the output device }
  552.         midiOutUnprepareHeader(FMidiHandle, thisHdr, sizeof(TMIDIHDR));
  553.  
  554.         { Get the address of the MyMidiHdr object containing this MIDIHDR structure.
  555.             We stored this address in the PutLong procedure }
  556.         MyMidiHdr := TMyMidiHdr(thisHdr^.dwUser);
  557.  
  558.         { Header and copy of sysex data no longer required since output is complete }
  559.         MyMidiHdr.Free;
  560.  
  561.         { Call the user's event handler if any }
  562.         if Assigned(FOnmidioutput) then
  563.             FOnmidioutput(Self);
  564.         end;
  565.     { TODO: Case for MOM_PLAYBACK_DONE }
  566. end;
  567.  
  568. {-------------------------------------------------------------------}
  569. procedure Register;
  570. begin
  571.   RegisterComponents('Samples', [Tmidioutput]);
  572. end;
  573.  
  574. end.
  575.